From 4315a113935902bbbb82047e3f43695b4d76fff2 Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Wed, 17 Jun 2015 15:38:00 +0100 Subject: Fixed and improved restarting Restarts are now an actual, close-as-possible to application exit+reopen. --- CIbuild.sh | 5 +- src/OSSupport/NetworkSingleton.cpp | 53 ++++--- src/OSSupport/NetworkSingleton.h | 11 +- src/Root.cpp | 291 ++++++++++++++++++++----------------- src/Root.h | 10 +- src/main.cpp | 65 ++++----- tests/Network/Google.cpp | 1 + 7 files changed, 239 insertions(+), 197 deletions(-) diff --git a/CIbuild.sh b/CIbuild.sh index 60fed73c2..2cfc4f887 100755 --- a/CIbuild.sh +++ b/CIbuild.sh @@ -17,6 +17,7 @@ echo "Building..." make -j 2; make -j 2 test ARGS="-V"; cd MCServer/; -if [ "$TRAVIS_MCSERVER_BUILD_TYPE" != "COVERAGE" ] - then echo stop | $MCSERVER_PATH; +if [ "$TRAVIS_MCSERVER_BUILD_TYPE" != "COVERAGE" ]; then + echo restart | $MCSERVER_PATH; + echo stop | $MCSERVER_PATH; fi diff --git a/src/OSSupport/NetworkSingleton.cpp b/src/OSSupport/NetworkSingleton.cpp index 417fab01e..c16f92c5a 100644 --- a/src/OSSupport/NetworkSingleton.cpp +++ b/src/OSSupport/NetworkSingleton.cpp @@ -18,8 +18,36 @@ -cNetworkSingleton::cNetworkSingleton(void): - m_HasTerminated(false) +cNetworkSingleton::cNetworkSingleton() : + m_HasTerminated(true) +{ +} + + + + + +cNetworkSingleton::~cNetworkSingleton() +{ + // Check that Terminate has been called already: + ASSERT(m_HasTerminated); +} + + + + + +cNetworkSingleton & cNetworkSingleton::Get(void) +{ + static cNetworkSingleton Instance; + return Instance; +} + + + + + +void cNetworkSingleton::Initialise(void) { // Windows: initialize networking: #ifdef _WIN32 @@ -64,26 +92,7 @@ cNetworkSingleton::cNetworkSingleton(void): // Create the event loop thread: m_EventLoopThread = std::thread(RunEventLoop, this); -} - - - - - -cNetworkSingleton::~cNetworkSingleton() -{ - // Check that Terminate has been called already: - ASSERT(m_HasTerminated); -} - - - - - -cNetworkSingleton & cNetworkSingleton::Get(void) -{ - static cNetworkSingleton Instance; - return Instance; + m_HasTerminated = false; } diff --git a/src/OSSupport/NetworkSingleton.h b/src/OSSupport/NetworkSingleton.h index 0536a1c82..c72df38ec 100644 --- a/src/OSSupport/NetworkSingleton.h +++ b/src/OSSupport/NetworkSingleton.h @@ -44,13 +44,18 @@ typedef std::vector cIPLookupPtrs; class cNetworkSingleton { public: + cNetworkSingleton(); ~cNetworkSingleton(); /** Returns the singleton instance of this class */ static cNetworkSingleton & Get(void); + /** Initialises all network-related threads. + To be called on first run or after app restart. */ + void Initialise(void); + /** Terminates all network-related threads. - To be used only on app shutdown. + To be used only on app shutdown or restart. MSVC runtime requires that the LibEvent networking be shut down before the main() function is exitted; this is the way to do it. */ void Terminate(void); @@ -122,10 +127,6 @@ protected: /** The thread in which the main LibEvent loop runs. */ std::thread m_EventLoopThread; - - /** Initializes the LibEvent internals. */ - cNetworkSingleton(void); - /** Converts LibEvent-generated log events into log messages in MCS log. */ static void LogCallback(int a_Severity, const char * a_Msg); diff --git a/src/Root.cpp b/src/Root.cpp index 624e95e18..222c799b2 100644 --- a/src/Root.cpp +++ b/src/Root.cpp @@ -26,10 +26,10 @@ #include #ifdef _WIN32 - #include #include #elif defined(__linux__) #include + #include #elif defined(__APPLE__) #include #endif @@ -39,7 +39,6 @@ cRoot * cRoot::s_Root = nullptr; -bool cRoot::m_ShouldStop = false; @@ -53,10 +52,10 @@ cRoot::cRoot(void) : m_FurnaceRecipe(nullptr), m_WebAdmin(nullptr), m_PluginManager(nullptr), - m_MojangAPI(nullptr), - m_bRestart(false) + m_MojangAPI(nullptr) { s_Root = this; + m_InputThreadRunFlag.clear(); } @@ -76,24 +75,22 @@ void cRoot::InputThread(cRoot & a_Params) { cLogCommandOutputCallback Output; - while (!cRoot::m_ShouldStop && !a_Params.m_bRestart && !m_TerminateEventRaised && std::cin.good()) + while (a_Params.m_InputThreadRunFlag.test_and_set() && std::cin.good()) { AString Command; std::getline(std::cin, Command); if (!Command.empty()) { + // Execute and clear command string when submitted a_Params.ExecuteConsoleCommand(TrimString(Command), Output); } } - if (m_TerminateEventRaised || !std::cin.good()) + // We have come here because the std::cin has received an EOF / a terminate signal has been sent, and the server is still running + if (!std::cin.good()) { - // We have come here because the std::cin has received an EOF / a terminate signal has been sent, and the server is still running // Stop the server: - if (!m_RunAsService) // Dont kill if running as a service - { - a_Params.m_ShouldStop = true; - } + a_Params.QueueExecuteConsoleCommand("stop"); } } @@ -101,12 +98,12 @@ void cRoot::InputThread(cRoot & a_Params) -void cRoot::Start(std::unique_ptr overridesRepo) +void cRoot::Start(std::unique_ptr a_OverridesRepo) { #ifdef _WIN32 - HWND hwnd = GetConsoleWindow(); - HMENU hmenu = GetSystemMenu(hwnd, FALSE); - EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED); // Disable close button when starting up; it causes problems with our CTRL-CLOSE handling + HWND hwnd = GetConsoleWindow(); + HMENU hmenu = GetSystemMenu(hwnd, FALSE); + EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED); // Disable close button when starting up; it causes problems with our CTRL-CLOSE handling #endif cLogger::cListener * consoleLogListener = MakeConsoleListener(m_RunAsService); @@ -127,156 +124,197 @@ void cRoot::Start(std::unique_ptr overridesRepo) #endif cDeadlockDetect dd; + auto BeginTime = std::chrono::steady_clock::now(); - m_ShouldStop = false; - while (!m_ShouldStop) - { - auto BeginTime = std::chrono::steady_clock::now(); - m_bRestart = false; - - LoadGlobalSettings(); + LoadGlobalSettings(); - LOG("Creating new server instance..."); - m_Server = new cServer(); + LOG("Creating new server instance..."); + m_Server = new cServer(); - LOG("Reading server config..."); + LOG("Reading server config..."); - auto IniFile = cpp14::make_unique(); - if (!IniFile->ReadFile("settings.ini")) - { - LOGWARN("Regenerating settings.ini, all settings will be reset"); - IniFile->AddHeaderComment(" This is the main server configuration"); - IniFile->AddHeaderComment(" Most of the settings here can be configured using the webadmin interface, if enabled in webadmin.ini"); - IniFile->AddHeaderComment(" See: http://wiki.mc-server.org/doku.php?id=configure:settings.ini for further configuration help"); - } - auto settingsRepo = cpp14::make_unique(std::move(IniFile), std::move(overridesRepo)); + auto IniFile = cpp14::make_unique(); + if (!IniFile->ReadFile("settings.ini")) + { + LOGWARN("Regenerating settings.ini, all settings will be reset"); + IniFile->AddHeaderComment(" This is the main server configuration"); + IniFile->AddHeaderComment(" Most of the settings here can be configured using the webadmin interface, if enabled in webadmin.ini"); + IniFile->AddHeaderComment(" See: http://wiki.mc-server.org/doku.php?id=configure:settings.ini for further configuration help"); + } + auto settingsRepo = cpp14::make_unique(std::move(IniFile), std::move(a_OverridesRepo)); - LOG("Starting server..."); - m_MojangAPI = new cMojangAPI; - bool ShouldAuthenticate = settingsRepo->GetValueSetB("Authentication", "Authenticate", true); - m_MojangAPI->Start(*settingsRepo, ShouldAuthenticate); // Mojang API needs to be started before plugins, so that plugins may use it for DB upgrades on server init - if (!m_Server->InitServer(*settingsRepo, ShouldAuthenticate)) - { - settingsRepo->Flush(); - LOGERROR("Failure starting server, aborting..."); - return; - } + LOG("Starting server..."); + m_MojangAPI = new cMojangAPI; + bool ShouldAuthenticate = settingsRepo->GetValueSetB("Authentication", "Authenticate", true); + m_MojangAPI->Start(*settingsRepo, ShouldAuthenticate); // Mojang API needs to be started before plugins, so that plugins may use it for DB upgrades on server init + if (!m_Server->InitServer(*settingsRepo, ShouldAuthenticate)) + { + settingsRepo->Flush(); + LOGERROR("Failure starting server, aborting..."); + return; + } - m_WebAdmin = new cWebAdmin(); - m_WebAdmin->Init(); + m_WebAdmin = new cWebAdmin(); + m_WebAdmin->Init(); - LOGD("Loading settings..."); - m_RankManager.reset(new cRankManager()); - m_RankManager->Initialize(*m_MojangAPI); - m_CraftingRecipes = new cCraftingRecipes; - m_FurnaceRecipe = new cFurnaceRecipe(); + LOGD("Loading settings..."); + m_RankManager.reset(new cRankManager()); + m_RankManager->Initialize(*m_MojangAPI); + m_CraftingRecipes = new cCraftingRecipes(); + m_FurnaceRecipe = new cFurnaceRecipe(); - LOGD("Loading worlds..."); - LoadWorlds(*settingsRepo); + LOGD("Loading worlds..."); + LoadWorlds(*settingsRepo); - LOGD("Loading plugin manager..."); - m_PluginManager = new cPluginManager(); - m_PluginManager->ReloadPluginsNow(*settingsRepo); + LOGD("Loading plugin manager..."); + m_PluginManager = new cPluginManager(); + m_PluginManager->ReloadPluginsNow(*settingsRepo); - LOGD("Loading MonsterConfig..."); - m_MonsterConfig = new cMonsterConfig; + LOGD("Loading MonsterConfig..."); + m_MonsterConfig = new cMonsterConfig; - // This sets stuff in motion - LOGD("Starting Authenticator..."); - m_Authenticator.Start(*settingsRepo); + // This sets stuff in motion + LOGD("Starting Authenticator..."); + m_Authenticator.Start(*settingsRepo); - LOGD("Starting worlds..."); - StartWorlds(); + LOGD("Starting worlds..."); + StartWorlds(); - if (settingsRepo->GetValueSetB("DeadlockDetect", "Enabled", true)) - { - LOGD("Starting deadlock detector..."); - dd.Start(settingsRepo->GetValueSetI("DeadlockDetect", "IntervalSec", 20)); - } + if (settingsRepo->GetValueSetB("DeadlockDetect", "Enabled", true)) + { + LOGD("Starting deadlock detector..."); + dd.Start(settingsRepo->GetValueSetI("DeadlockDetect", "IntervalSec", 20)); + } - settingsRepo->Flush(); + settingsRepo->Flush(); - LOGD("Finalising startup..."); - if (m_Server->Start()) - { - m_WebAdmin->Start(); + LOGD("Finalising startup..."); + if (m_Server->Start()) + { + m_WebAdmin->Start(); - #if !defined(ANDROID_NDK) + #if !defined(ANDROID_NDK) LOGD("Starting InputThread..."); try { + m_InputThreadRunFlag.test_and_set(); m_InputThread = std::thread(InputThread, std::ref(*this)); - m_InputThread.detach(); } catch (std::system_error & a_Exception) { LOGERROR("cRoot::Start (std::thread) error %i: could not construct input thread; %s", a_Exception.code().value(), a_Exception.what()); } - #endif + #endif - LOG("Startup complete, took %ldms!", static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - BeginTime).count())); + LOG("Startup complete, took %ldms!", static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - BeginTime).count())); - // Save the current time - m_StartTime = std::chrono::steady_clock::now(); + // Save the current time + m_StartTime = std::chrono::steady_clock::now(); - #ifdef _WIN32 + #ifdef _WIN32 EnableMenuItem(hmenu, SC_CLOSE, MF_ENABLED); // Re-enable close button - #endif + #endif + + for (;;) + { + m_StopEvent.Wait(); - while (!m_ShouldStop && !m_bRestart && !m_TerminateEventRaised) // These are modified by external threads + if (m_TerminateEventRaised && m_RunAsService) { - std::this_thread::sleep_for(std::chrono::seconds(1)); + // Dont kill if running as a service + m_TerminateEventRaised = false; } - - if (m_TerminateEventRaised) + else { - m_ShouldStop = true; + break; } + } - // Stop the server: - m_WebAdmin->Stop(); + // Stop the server: + m_WebAdmin->Stop(); - LOG("Shutting down server..."); - m_Server->Shutdown(); - } // if (m_Server->Start()) - else - { - m_ShouldStop = true; - } + LOG("Shutting down server..."); + m_Server->Shutdown(); + } // if (m_Server->Start() - delete m_MojangAPI; m_MojangAPI = nullptr; + delete m_MojangAPI; m_MojangAPI = nullptr; - LOGD("Shutting down deadlock detector..."); - dd.Stop(); + LOGD("Shutting down deadlock detector..."); + dd.Stop(); - LOGD("Stopping world threads..."); - StopWorlds(); + LOGD("Stopping world threads..."); + StopWorlds(); - LOGD("Stopping authenticator..."); - m_Authenticator.Stop(); + LOGD("Stopping authenticator..."); + m_Authenticator.Stop(); - LOGD("Freeing MonsterConfig..."); - delete m_MonsterConfig; m_MonsterConfig = nullptr; - delete m_WebAdmin; m_WebAdmin = nullptr; + LOGD("Freeing MonsterConfig..."); + delete m_MonsterConfig; m_MonsterConfig = nullptr; + delete m_WebAdmin; m_WebAdmin = nullptr; - LOGD("Unloading recipes..."); - delete m_FurnaceRecipe; m_FurnaceRecipe = nullptr; - delete m_CraftingRecipes; m_CraftingRecipes = nullptr; + LOGD("Unloading recipes..."); + delete m_FurnaceRecipe; m_FurnaceRecipe = nullptr; + delete m_CraftingRecipes; m_CraftingRecipes = nullptr; - LOG("Unloading worlds..."); - UnloadWorlds(); + LOG("Unloading worlds..."); + UnloadWorlds(); - LOGD("Stopping plugin manager..."); - delete m_PluginManager; m_PluginManager = nullptr; + LOGD("Stopping plugin manager..."); + delete m_PluginManager; m_PluginManager = nullptr; - cItemHandler::Deinit(); + cItemHandler::Deinit(); - LOG("Cleaning up..."); - delete m_Server; m_Server = nullptr; + LOG("Cleaning up..."); + delete m_Server; m_Server = nullptr; + m_InputThreadRunFlag.clear(); + #ifdef _WIN32 + DWORD Length; + INPUT_RECORD Record + { + static_cast(KEY_EVENT), + { + { + TRUE, + 1, + VK_RETURN, + MapVirtualKey(VK_RETURN, MAPVK_VK_TO_VSC), + { { VK_RETURN } }, + 0 + } + } + }; + + // Can't kill the input thread since it breaks cin (getline doesn't block / receive input on restart) + // Apparently no way to unblock getline + // Only thing I can think of for now + if (WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE), &Record, 1, &Length) == 0) + { + LOGWARN("Couldn't notify the input thread; the server will hang before shutdown!"); + m_TerminateEventRaised = true; + m_InputThread.detach(); + } + else + { + m_InputThread.join(); + } + #else + if (pthread_kill(m_InputThread.native_handle(), SIGKILL) != 0) + { + LOGWARN("Couldn't notify the input thread; the server will hang before shutdown!"); + m_TerminateEventRaised = true; + m_InputThread.detach(); + } + #endif + + if (m_TerminateEventRaised) + { LOG("Shutdown successful!"); } - + else + { + LOG("Shutdown successful - restarting..."); + } LOG("--- Stopped Log ---"); cLogger::GetInstance().DetachListener(consoleLogListener); @@ -475,19 +513,9 @@ void cRoot::TickCommands(void) void cRoot::QueueExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) { - // Some commands are built-in: - if (a_Cmd == "stop") - { - m_ShouldStop = true; - } - else if (a_Cmd == "restart") - { - m_bRestart = true; - } - // Put the command into a queue (Alleviates FS #363): cCSLock Lock(m_CSPendingCommands); - m_PendingCommands.push_back(cCommand(a_Cmd, &a_Output)); + m_PendingCommands.emplace_back(a_Cmd, &a_Output); } @@ -507,15 +535,18 @@ void cRoot::QueueExecuteConsoleCommand(const AString & a_Cmd) void cRoot::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallback & a_Output) { - // cRoot handles stopping and restarting due to our access to controlling variables + // Some commands are built-in: if (a_Cmd == "stop") { - m_ShouldStop = true; + m_TerminateEventRaised = true; + m_StopEvent.Set(); + m_InputThreadRunFlag.clear(); return; } else if (a_Cmd == "restart") { - m_bRestart = true; + m_StopEvent.Set(); + m_InputThreadRunFlag.clear(); return; } diff --git a/src/Root.h b/src/Root.h index b29fe0a5e..772d858d9 100644 --- a/src/Root.h +++ b/src/Root.h @@ -7,6 +7,7 @@ #include "Defines.h" #include "RankManager.h" #include +#include @@ -48,13 +49,12 @@ public: static bool m_TerminateEventRaised; static bool m_RunAsService; - static bool m_ShouldStop; cRoot(void); ~cRoot(); - void Start(std::unique_ptr overridesRepo); + void Start(std::unique_ptr a_OverridesRepo); // tolua_begin cServer * GetServer(void) { return m_Server; } @@ -200,6 +200,8 @@ private: cCommandQueue m_PendingCommands; std::thread m_InputThread; + cEvent m_StopEvent; + std::atomic_flag m_InputThreadRunFlag; cServer * m_Server; cMonsterConfig * m_MonsterConfig; @@ -213,9 +215,7 @@ private: std::unique_ptr m_RankManager; - cHTTPServer m_HTTPServer; - - bool m_bRestart; + cHTTPServer m_HTTPServer; void LoadGlobalSettings(); diff --git a/src/main.cpp b/src/main.cpp index d0a5eb203..bc2439616 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,9 +22,6 @@ /** If something has told the server to stop; checked periodically in cRoot */ bool cRoot::m_TerminateEventRaised = false; -/** Set to true when the server terminates, so our CTRL handler can then tell the OS to close the console. */ -static bool g_ServerTerminated = false; - /** If set to true, the protocols will log each player's incoming (C->S) communication to a per-connection logfile */ bool g_ShouldLogCommIn; @@ -72,7 +69,7 @@ Synchronize this with Server.cpp to enable the "dumpmem" console command. */ void NonCtrlHandler(int a_Signal) { LOGD("Terminate event raised from std::signal"); - cRoot::m_TerminateEventRaised = true; + cRoot::Get()->QueueExecuteConsoleCommand("stop"); switch (a_Signal) { @@ -189,13 +186,11 @@ LONG WINAPI LastChanceExceptionFilter(__in struct _EXCEPTION_POINTERS * a_Except // Handle CTRL events in windows, including console window close BOOL CtrlHandler(DWORD fdwCtrlType) { - cRoot::m_TerminateEventRaised = true; + cRoot::Get()->QueueExecuteConsoleCommand("stop"); LOGD("Terminate event raised from the Windows CtrlHandler"); - while (!g_ServerTerminated) - { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); // Delay as much as possible to try to get the server to shut down cleanly - } + std::this_thread::sleep_for(std::chrono::seconds(10)); // Delay as much as possible to try to get the server to shut down cleanly - 10 seconds given by Windows + // Returning from main() automatically aborts this handler thread return TRUE; } @@ -206,29 +201,22 @@ BOOL CtrlHandler(DWORD fdwCtrlType) //////////////////////////////////////////////////////////////////////////////// -// universalMain - Main startup logic for both standard running and as a service +// UniversalMain - Main startup logic for both standard running and as a service -void universalMain(std::unique_ptr overridesRepo) +void UniversalMain(std::unique_ptr a_OverridesRepo) { - #ifdef _WIN32 - if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE)) - { - LOGERROR("Could not install the Windows CTRL handler!"); - } - #endif - // Initialize logging subsystem: cLogger::InitiateMultithreading(); // Initialize LibEvent: - cNetworkSingleton::Get(); + cNetworkSingleton::Get().Initialise(); #if !defined(ANDROID_NDK) try #endif { cRoot Root; - Root.Start(std::move(overridesRepo)); + Root.Start(std::move(a_OverridesRepo)); } #if !defined(ANDROID_NDK) catch (std::exception & e) @@ -241,8 +229,6 @@ void universalMain(std::unique_ptr overridesRepo) } #endif - g_ServerTerminated = true; - // Shutdown all of LibEvent: cNetworkSingleton::Get().Terminate(); } @@ -259,8 +245,11 @@ DWORD WINAPI serviceWorkerThread(LPVOID lpParam) { UNREFERENCED_PARAMETER(lpParam); - // Do the normal startup - universalMain(cpp14::make_unique()); + while (!cRoot::m_TerminateEventRaised) + { + // Do the normal startup + UniversalMain(cpp14::make_unique()); + } return ERROR_SUCCESS; } @@ -274,8 +263,7 @@ DWORD WINAPI serviceWorkerThread(LPVOID lpParam) void serviceSetState(DWORD acceptedControls, DWORD newState, DWORD exitCode) { - SERVICE_STATUS serviceStatus; - ZeroMemory(&serviceStatus, sizeof(SERVICE_STATUS)); + SERVICE_STATUS serviceStatus = {}; serviceStatus.dwCheckPoint = 0; serviceStatus.dwControlsAccepted = acceptedControls; serviceStatus.dwCurrentState = newState; @@ -302,11 +290,10 @@ void WINAPI serviceCtrlHandler(DWORD CtrlCode) { case SERVICE_CONTROL_STOP: { - cRoot::m_ShouldStop = true; + cRoot::Get()->QueueExecuteConsoleCommand("stop"); serviceSetState(0, SERVICE_STOP_PENDING, 0); break; } - default: { break; @@ -365,7 +352,7 @@ void WINAPI serviceMain(DWORD argc, TCHAR *argv[]) -std::unique_ptr parseArguments(int argc, char **argv) +std::unique_ptr ParseArguments(int argc, char **argv) { try { @@ -484,7 +471,13 @@ int main(int argc, char **argv) #endif // SIGABRT_COMPAT #endif - auto argsRepo = parseArguments(argc, argv); + + #ifdef _WIN32 + if (!SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlHandler, TRUE)) + { + LOGERROR("Could not install the Windows CTRL handler!"); + } + #endif // Attempt to run as a service if (cRoot::m_RunAsService) @@ -522,13 +515,19 @@ int main(int argc, char **argv) close(STDOUT_FILENO); close(STDERR_FILENO); - universalMain(std::move(argsRepo)); + while (!cRoot::m_TerminateEventRaised) + { + UniversalMain(std::move(ParseArguments(argc, argv))); + } #endif } else { - // Not running as a service, do normal startup - universalMain(std::move(argsRepo)); + while (!cRoot::m_TerminateEventRaised) + { + // Not running as a service, do normal startup + UniversalMain(std::move(ParseArguments(argc, argv))); + } } #if defined(_MSC_VER) && defined(_DEBUG) && defined(ENABLE_LEAK_FINDER) diff --git a/tests/Network/Google.cpp b/tests/Network/Google.cpp index ab366f9e2..23017d23b 100644 --- a/tests/Network/Google.cpp +++ b/tests/Network/Google.cpp @@ -118,6 +118,7 @@ static void DoTest(void) int main() { + cNetworkSingleton::Get().Initialise(); DoTest(); cNetworkSingleton::Get().Terminate(); -- cgit v1.2.3